package com.cordys.opensource.crom;

import java.util.Enumeration;

import com.cordys.cpc.bsf.busobject.BusObject;
import com.cordys.cpc.bsf.busobject.BusObjectIterator;
import com.cordys.cpc.bsf.classinfo.ClassInfo;
import com.cordys.cpc.bsf.classinfo.RelationInfo_Composite;
import com.cordys.cpc.bsf.event.AfterAbortObjectEvent;
import com.cordys.cpc.bsf.event.AfterCommitObjectEvent;
import com.cordys.cpc.bsf.event.BeforeAbortObjectEvent;
import com.cordys.cpc.bsf.event.BeforeCommitObjectEvent;
import com.cordys.cpc.bsf.event.BusObjectEvent;
import com.cordys.cpc.bsf.event.BusObjectUpdateEvent;
import com.cordys.cpc.bsf.relation.MultiRelation;
import com.cordys.cpc.bsf.relation.SingleRelation;

/**
 * This is a util library to keep track of 
 * the various persistence related BusObjects for a domain object while it is in a WS-AppServer transaction.
 * 
 * <br/>
 * In WS-AppServer we have 2 packages. One containing the DB-generated classes, and one containing the
 * domain object classes. The latter package is created by hand. The classes from that package are used inside
 * the UI, BPM, etc.
 * <p>
 * The DomainObjectInformation class manages the relationship between an object in the domain package and it's 
 * corresponding objects from the db package.
 * <p>
 * It does this by listening to WS-AppServer events on the domain object. 
 * If there is metadata available for the domain object, it will use that to invoke the right behavior on the related
 * database objects.
 * 
 * <p>
 * Important implementation details:
 * <ul>
 * <li>domain objects are registered within their transaction context, in
 * order to achieve isolation and consistency.</li>
 * <li>each domain object points to a DomainObjectPersistence object; this contains the corresponding database
 * records for the domain object</li>
 * <li>if two domain objects within the same transaction have the same semantic key, they will point to the same
 * DomainObjectPersistence instance; E.g., if two rules would fire on CalculationPosition 13, this may lead to 
 * 2 domain object instances of CalculationPosition within the same transaction context. Both of them will point to 
 * the same database record and the same revision record.</li> 
 * <li>each domain object refers the logic of updating to its listeners; depending on the interfaces (DomainObject, RevisionedObject)
 *  they implement, a domain object may have a DomainObjectEventListener and a RevisionObjectEventListener</li>
 * </ul>
 * 
 * 
 * @see com.cordys.opensource.crom.DomainObject
 * @see com.cordys.opensource.crom.RevisionedObject
 * @see com.cordys.opensource.crom.BusObjectMetadata
 * @see com.cordys.opensource.crom.DomainObjectPersistence
 * @see com.cordys.opensource.crom.TransactionContext
 * @see com.cordys.opensource.crom.DomainObjectEventListener
 * @see com.cordys.opensource.crom.RevisionObjectEventListener
 *
 */
public class BusObjectWrapper
{
	private final BusObject domainObject;
	private BusObject domainParent;
	private BusObjectMetadata metadata;
	private final TransactionContext transactionContext; // The transaction in which this object resides

	private DomainObjectEventListener domainObjectEventListener; // Holds a pointer to the event handler for the wrapped BusObject (only in case it is a @DomainObject)
	private DomainObjectPersistence domainObjectPersistence; // Holds a pointer to the database objects of the domain object. This is just for ease of debugging
	
	BusObjectWrapper(BusObject domainObject, TransactionContext tc)
	{
		this.domainObject = domainObject;
		this.transactionContext = tc;
	}
	
	private void setMetadata(BusObjectMetadata metadata)
	{
		if (this.metadata==null)
		{
			this.metadata = metadata;
			if (metadata!=null)
			{
				this.domainObjectEventListener = new DomainObjectEventListener(this, metadata);
			}
		}
	}
	
	/**
	 * Indicates whether this is a masterdata object
	 * @return
	 */
	boolean isMasterData()
	{
		return BusObjectMetadata.isMasterDataObject(domainObject.getClass());
	}
	
	/**
	 * Returns the database object for the corresponding domain object. E.g., for a <code>crom.example.domain.Ticket</code> object,
	 * it would return a <code>crom.example.db.Ticket</code> object, if one is registered. Registration will only happen in case
	 * of updates or inserts on a domain object.
	 * @param domainObject
	 * @return the corresponding db object for the domain object if it was created.
	 */
	@SuppressWarnings("unchecked") // Auto-Casting for convenience
	public static <T extends BusObject> T getDatabaseObject(BusObject domainObject)
	{
		// Warning for unsafe typecast is suppressed; The <T> parameter is only added to make it more easy to do the typecasting
		//   in the code invoking this API (reason: long package names). 
		BusObjectWrapper bow = getBusObjectWrapper(domainObject);
		T result = (T) bow.getPersistenceMapping().getDatabaseObject();
		if (result==null && bow.domainObjectEventListener!=null)
		{
			result = (T) bow.domainObjectEventListener.getDBObject();
		}
		return result;
	}
	
	/**
	 * Returns the mapping object for a corresponding domain object and it's parent. E.g., when a <code>crom.example.domain.User</code> object is
	 * inserted, and it is inside a <code>crom.example.domain.Project</code> object, then during the insert
	 * an instance of <code>crom.example.db.Project_Person</code> is created.
	 * @param domainObject
	 * @return the corresponding mapping object for the domain object if it was created.
	 */
	@SuppressWarnings("unchecked") // Auto-Casting for convenience
	public static <T extends BusObject> T getMappingObject(BusObject domainObject)
	{
		// Warning for unsafe typecast is suppressed; The <T> parameter is only added to make it more easy to do the typecasting
		//   in the code invoking this API (reason: long package names). 
		BusObjectWrapper bow = getBusObjectWrapper(domainObject);
		T result = (T) bow.getPersistenceMapping().getMappingObject();
		if (result==null && bow.domainObjectEventListener!=null)
		{
			result = (T) bow.domainObjectEventListener.getDBMappingObject();
		}
		return result;
	}
	
	/**
	 * Returns the database revision object for the corresponding domain object. E.g., for a <code>crom.example.domain.Solution</code> object,
	 * it would return a <code>crom.example.db.SolutionRevision</code> object, if one is registered. Registration will only happen in case
	 * of updates or inserts on a domain object.
	 * @param domainObject
	 * @return the corresponding db revision object for the domain object if it was created.
	 */
	@SuppressWarnings("unchecked") // Auto-Casting for convenience
	public static <T extends BusObject> T getDatabaseRevisionObject(BusObject domainObject)
	{
		// Warning for unsafe typecast is suppressed; The <T> parameter is only added to make it more easy to do the typecasting
		//   in the code invoking this API (reason: long package names).
		BusObjectWrapper bow = getBusObjectWrapper(domainObject);
		T result = (T) bow.getPersistenceMapping().getNewRevision();
		if (result==null && bow.domainObjectEventListener!=null)
		{
			RevisionObjectEventListener roel = bow.domainObjectEventListener.getRevisionObjectEventListener();
			if (roel!=null)
			{
				result = (T) roel.getNewRevision();
				BusObjectHelper.mapDBClassAttributes(result, domainObject, false);
			}
		}
		return result;
	}
	
	/**
	 * Returns the parent object for the corresponding domain object. E.g., for a <code>crom.example.domain.User</code> object,
	 * it would return a <code>crom.example.domain.Ticket</code> object, if the User is part of the Ticket. Registration will only happen in case
	 * of updates or inserts on the parent domain object.
	 * @param domainObject
	 * @return the corresponding parent for the domain object if it is present.
	 */
	@SuppressWarnings("unchecked") // Auto-Casting for convenience
	public static <T extends BusObject> T getDomainParent(BusObject domainObject)
	{
		return (T) BusObjectWrapper.getBusObjectWrapper(domainObject).getDomainParent();
	}

	/**
	 * Returns the name of the relation of this object in it's parent.
	 * @param domainObject
	 */
	public static String getRelationNameInParent(BusObject domainObject)
	{
		BusObjectWrapper wrapper = getBusObjectWrapper(domainObject);
		if (wrapper.domainParent!=null)
		{
			return wrapper.metadata.getParentRelationship().getName();
		}
		else
		{
			throw new NullPointerException("Domain object has a null parent\n"+domainObject);
		}
	}

	/**
	 * Retrieves a DomainObjectWrapper for the specified BusObject from the registration. Registers it if it has not yet been registered.
	 * @param domainObject
	 * @return
	 */
	private static BusObjectWrapper getBusObjectWrapper(BusObject domainObject)
	{
		BusObjectWrapper wrapper = TransactionContext.getDomainObjectWrapper(domainObject);
		if (wrapper.metadata==null && BusObjectMetadata.isDomainObject(domainObject.getClass()))
		{
			wrapper.getDomainParent();
		}
		return wrapper;
	}
	
	DomainObjectPersistence getPersistenceMapping()
	{
		if (domainObjectPersistence==null)
		{
			domainObjectPersistence = transactionContext.getPersistenceMapping(domainObject);
		}
		return domainObjectPersistence;
	}
	
	/**
	 * Returns the domain object that is wrapped by this class.
	 * @return
	 */
	BusObject getDomainObject()
	{
		return domainObject;
	}
	
	BusObject getDomainParent()
	{
		// Try to use the outer object in WS-AppServer.
		//  This however does not always work, since the BusObjectHelper during Update operations
		//  disconnects the nested children from their parent, in order to be able to invoke the update
		//  listeners on the nested children. BusObjectHelper then has to make sure it will set the domainParent of this wrapper.
		// When the domain parent is found through WS-AppServer, we will also create a wrapper for the parent object, and
		//  initialize the metadata for it.
		if (domainParent==null)
		{
			if (domainObject.sys_isInnerObject())
			{
				domainParent = domainObject.sys_getOuterObject();
				
				// Now recurse the whole tree and create wrappers for each object in the tree.
				BusObjectWrapper parentWrapper = TransactionContext.getDomainObjectWrapper(domainParent);
				if (parentWrapper.metadata==null && BusObjectMetadata.isDomainObject(domainParent.getClass())) // Note: this statement occurs twice.
				{
					// Apparently not yet initialized. Recurse the whole ancestor tree.
					parentWrapper.getDomainParent();
					// Now iterate all relations of the parent to find out in which one we belong.
					locateSelfInParent(parentWrapper);
				}
			}
			else // No parent, means we are the root object, and we can hence initialize the metadata.
			{
				setMetadata(BusObjectMetadata.getMetadata(domainObject.getClass()));
			}
		}
		return domainParent;
	}
	
	private void locateSelfInParent(BusObjectWrapper parentWrapper)
	{
		if (domainParent==null) // Shouldn't happen. But still check.
		{
			return;
		}
		ClassInfo parentInfo = BusObject._getClassInfo(domainParent.getClass());
		Enumeration<?> relations = parentInfo.getRelationInfos();
		while (relations.hasMoreElements())
		{
			Object next = relations.nextElement();
			if (next instanceof RelationInfo_Composite)
			{
				RelationInfo_Composite relation = (RelationInfo_Composite) next;
				if (BusObjectHelper.getRelatedClass(relation).equals(domainObject.getClass()))
					// Only relations in which my class occurs are interesting
				{
					String relationID = relation.getIdentifier();
					if (relation.isMultiOcc())
					{
						// Load the children, iterate them to see if we are amongst them.
						MultiRelation relationNewChildren = domainParent._getMultiRelation(relationID, true);
						relationNewChildren.load();
						@SuppressWarnings("unchecked") // Can it be something different?
						BusObjectIterator<BusObject> children = relationNewChildren.getObjects();
						while (children.hasMoreElements())
						{
							BusObject child = children.nextElement();
							if (child==domainObject)
							{
								// Gotcha!
								setMetadata(parentWrapper.metadata.getRelation(relation.getName()));
								return;
							}
						}
					}
					else
					{
						// Load the child, and check if it is us.
						SingleRelation relationNewChild = domainParent._getSingleRelation(relationID, true);
						relationNewChild.load();
						BusObject child = relationNewChild.getLocalObject();
						if (child==domainObject)
						{
							// gotcha
							setMetadata(parentWrapper.metadata.getRelation(relation.getName()));
							return;
						}
					}
				}
			}
		}
	}
	
	/**
	 * Internal method. To be invoked only from BusObjectHelper.
	 * @param domainParent
	 * @param domainObject
	 * @param relationID
	 */
	static void setDomainParent(BusObject domainParent, BusObject domainObject, String relationID)
	{
		if (domainObject==null)
		{
			return;
		}
		BusObjectWrapper myWrapper = TransactionContext.getDomainObjectWrapper(domainObject);
		myWrapper.domainParent = domainParent;
		if (myWrapper.metadata==null && BusObjectMetadata.isDomainObject(domainObject.getClass()))
		{
			BusObjectWrapper parentWrapper = TransactionContext.getDomainObjectWrapper(domainParent);
			// Invoke recursive function to make sure parent is filled with proper metadata.
			//  Notice: this is rather tricky code. Not sure how well balanced it is ...
			parentWrapper.getDomainParent();
			BusObjectMetadata parentMetadata = parentWrapper.metadata;
			if (parentMetadata!=null)
			{
				myWrapper.setMetadata(parentMetadata.getRelation(relationID));
			}
		}
	}
	
	static void onBeforeInsert(BusObjectEvent event)
	{
		BusObject domainObject = event.getObject();
		BusObjectWrapper dow = BusObjectWrapper.getBusObjectWrapper(domainObject);
		
		if (dow.domainObjectEventListener!=null) dow.domainObjectEventListener.onBeforeInsert();

		// Also insert all children.
		BusObjectHelper.insertChildren(domainObject);
	}
	
	static void onAfterInsert(BusObjectEvent event)
	{
		BusObject domainObject = event.getObject();
		BusObjectWrapper bow = BusObjectWrapper.getBusObjectWrapper(domainObject);
		
		if (bow.domainObjectEventListener!=null) bow.domainObjectEventListener.onAfterInsert();
	}

	static void onBeforeUpdate(BusObjectUpdateEvent event)
	{
		BusObject domainObject = event.getObject();
		
		BusObjectWrapper bow = BusObjectWrapper.getBusObjectWrapper(domainObject);
		
		if (bow.domainObjectEventListener!=null) bow.domainObjectEventListener.onBeforeUpdate();
		
		
		// This is kind of a hack, because analyseAndUpdateChildren assumes always that
		//  old and new are available. But in some weird cases this does not happen, hence returning...
		BusObject original = domainObject.getOriginalObject();
		if (original==null)
		{
			// Is this an insert?? :) Cannot do anything with it, because basically there is not yet any update available.
			// Typically may happen when Ticket.update() is invoked on an object that is newly created.
			//  We should return here, otherwise all children are treated as "Insert", which is wrong (this should
			//  be invoked via the insert method, not via the update method).
			return;
		}
		
		// Also update all children. This code will analyse for each child
		//  whether it has to be update, inserted or deleted.
		BusObjectHelper.updateChildren(domainObject, original);
	}

	static void onAfterUpdate(BusObjectUpdateEvent event)
	{
		BusObject domainObject = event.getObject();
		BusObjectWrapper bow = BusObjectWrapper.getBusObjectWrapper(domainObject);
		
		if (bow.domainObjectEventListener!=null) bow.domainObjectEventListener.onAfterUpdate();
	}


	static void onBeforeDelete(BusObjectEvent event)
	{
		BusObject domainObject = event.getObject();
		BusObjectWrapper bow = BusObjectWrapper.getBusObjectWrapper(domainObject);
		
		if (bow.domainObjectEventListener!=null) bow.domainObjectEventListener.onBeforeDelete();
	
		// Also delete all children if the object is not a MasterData object.		
		if (!bow.isMasterData()) 
		{		
		    BusObjectHelper.deleteChildren(domainObject);
		}
	}

	static void onAfterDelete(BusObjectEvent event)
	{
		BusObject domainObject = event.getObject();
		BusObjectWrapper dow = BusObjectWrapper.getBusObjectWrapper(domainObject);
		
		if (dow.domainObjectEventListener!=null) dow.domainObjectEventListener.onAfterDelete();
	}

	static void onBeforeAbort(BeforeAbortObjectEvent event)
	{
		TransactionContext.beforeAbort(event);
	}

	public static void onBeforeCommit(BeforeCommitObjectEvent event)
	{
		TransactionContext.beforeCommit(event);
	}
	
	public static void onAfterAbort(AfterAbortObjectEvent event)
	{
		TransactionContext.afterAbort(event);
	}

	public static void onAfterCommit(AfterCommitObjectEvent event)
	{
		TransactionContext.afterCommit(event);
	}
	
	void onBeforeCommit()
	{
		if (domainObjectEventListener!=null) domainObjectEventListener.onBeforeCommit();
	}

	void onBeforeAbort()
	{
		if (domainObjectEventListener!=null) domainObjectEventListener.onBeforeAbort();
	}
	
	void onAfterCommit()
	{
		if (domainObjectEventListener!=null) domainObjectEventListener.onAfterCommit();
	}
	
	void onAfterAbort()
	{
		if (domainObjectEventListener!=null) domainObjectEventListener.onAfterAbort();
	}
}
